<template>
  <el-dialog
    title="文件分片上传"
    :visible.sync="visible"
    :before-close="closeDialog"
    append-to-body
    width="50%"
  >
    <el-card style="width: 100%; margin: 0 auto">
      <el-upload
        class="upload-demo"
        drag
        action="/"
        multiple
        ref="upload"
        :file-list="fileList"
        :http-request="handleHttpRequest"
        :before-remove="handleRemoveFile"
      >
        <el-icon class="el-icon--upload"><upload-filled /></el-icon>
        <div class="el-upload__text">
          请拖拽文件到此处或 <em>点击此处上传</em>
        </div>
      </el-upload>
    </el-card>
    <span slot="footer" class="dialog-footer">
      <el-button @click="closeDialog" size="mini" :loading="loading"
        >取消</el-button
      >
      <el-button
        type="primary"
        @click="closeDialog"
        size="mini"
        :loading="loading"
        >确定</el-button
      >
    </span>
  </el-dialog>
</template>

<script>
import * as api from "@/api/fileList/index";
import request from "@/router/axios";
import SparkMD5 from "spark-md5";
import Queue from "promise-queue-plus";
import {EventBus} from "@/components/javaScript/EventBus";

const DEFAULT_SIZE = 100 * 1024 * 1024;
// import { ref } from "vue";
// 文件上传分块任务的队列（用于移除文件时，停止该文件的上传队列） key：fileUid value： queue object
let fileUploadChunkQueue = [];
export default {
  name: "uploadFile",
  props: {
    fileVisible: Boolean,
    updateLoading:{
      type: Function,
      default:()=>{}
    }
  },
  data() {
    return {
      visible: false,
      loading: false,
      fileList: [],
      isAbort: false,
      updateList: false,
    };
  },
  watch: {
      "$route.path": {
        handler(val) {
          if (val && val.indexOf('/fileList/fileList') > -1) {
            // 在文件管理页面刷新列表
            this.updateList= true;
          }else{
            this.updateList= false;
          }
        },
        deep: true,
        immediate: true,
      },

    fileVisible: {
      handler(val) {
        this.visible = val;
        fileUploadChunkQueue = [];
      },
      deep: true,
      immediate: true,
    },
    loading: {
      handler(val) {
        this.updateLoading(val);

      },
      deep: true,
      immediate: true,
    },
  },
  methods: {
    getMd5(file, chunkSize = DEFAULT_SIZE) {
      const that = this;
      return new Promise((resolve, reject) => {
        const startMs = new Date().getTime();
        let blobSlice =
          File.prototype.slice ||
          File.prototype.mozSlice ||
          File.prototype.webkitSlice;
        let chunks = Math.ceil(file.size / chunkSize);
        let currentChunk = 0;
        let spark = new SparkMD5.ArrayBuffer(); //追加数组缓冲区。
        let fileReader = new FileReader(); //读取文件
        fileReader.onload = function (e) {
          spark.append(e.target.result);
          currentChunk++;
          if (currentChunk < chunks) {
            loadNext();
          } else {
            const md5 = spark.end(); //完成md5的计算，返回十六进制结果。
            console.log(
              "文件md5计算结束，总耗时：",
              (new Date().getTime() - startMs) / 1000,
              "s"
            );
            resolve(md5);
          }
        };
        fileReader.onerror = function (e) {
          reject(e);
        };

        function loadNext() {
          if (that.isAbort) {
            reject({});
            return;
          }
          console.log("当前part number：", currentChunk, "总块数：", chunks);
          let start = currentChunk * chunkSize;
          let end = start + chunkSize;
          end > file.size && (end = file.size);
          fileReader.readAsArrayBuffer(blobSlice.call(file, start, end));
        }

        loadNext();
      });
    },

    closeDialog() {
      this.visible = false;
      this.$emit("update:fileVisible", false);
    },
    /**
     * 获取一个上传任务，没有则初始化一个
     */
    async getTaskInfo(file) {
      let task;
      const identifier = await this.getMd5(file);
      const {
        data: { code, data, msg },
      } = await api.taskInfo(identifier);
      if (code === 200) {
        task = data;
        if (!task || Object.keys(task) == 0) {
          const initTaskData = {
            identifier,
            fileName: file.name,
            totalSize: file.size,
            chunkSize: 5 * 1024 * 1024,
          };
          const {
            data: { code, data, msg },
          } = await api.initTask(initTaskData);
          if (code === 200) {
            task = data;
          } else {
            this.$message.error({
              title: "文件上传错误",
              message: msg,
            });
          }
        }
      } else {
        this.$message.error({
          title: "文件上传错误",
          message: msg,
        });
      }
      return task;
    },

    /**
     * 上传逻辑处理，如果文件已经上传完成（完成分块合并操作），则不会进入到此方法中
     */
    handleUpload(file, taskRecord, options) {
      let lastUploadedSize = 0; // 上次断点续传时上传的总大小
      let uploadedSize = 0; // 已上传的大小
      const totalSize = file.size || 0; // 文件总大小
      let startMs = new Date().getTime(); // 开始上传的时间
      const { exitPartList, chunkSize, chunkNum, fileIdentifier } = taskRecord;

      // 获取从开始上传到现在的平均速度（byte/s）
      const getSpeed = () => {
        // 已上传的总大小 - 上次上传的总大小（断点续传）= 本次上传的总大小（byte）
        const intervalSize = uploadedSize - lastUploadedSize;
        const nowMs = new Date().getTime();
        // 时间间隔（s）
        const intervalTime = (nowMs - startMs) / 1000;
        return intervalSize / intervalTime;
      };
      const that = this;
      if (that.isAbort) {
        that.$message.success("终止成功");
        return new Promise((resolve) => {
          resolve();
        });
      }
      const uploadNext = async (partNumber) => {
        const start = new Number(chunkSize) * (partNumber - 1);
        const end = start + new Number(chunkSize);
        const blob = file.slice(start, end);
        if (that.isAbort) {
          return Promise.reject("终止成功");
        }
        const {
          data: { code, data, msg },
        } = await api.preSignUrl({
          identifier: fileIdentifier,
          partNumber: partNumber,
        });
        if (code === 200 && data) {
          await request({
            url: data,
            method: "PUT",
            data: blob,
            headers: { "Content-Type": "application/octet-stream" },
          });
          return Promise.resolve({
            partNumber: partNumber,
            uploadedSize: blob.size,
          });
        }
        return Promise.reject(`分片${partNumber}， 获取上传地址失败`);
      };

      /**
       * 更新上传进度
       * @param increment 为已上传的进度增加的字节量
       */
      const updateProcess = (increment) => {
        increment = new Number(increment);
        const { onProgress } = options;
        let factor = 1000; // 每次增加1000 byte
        let from = 0;
        // 通过循环一点一点的增加进度
        while (from <= increment) {
          from += factor;
          uploadedSize += factor;
          const percent = Math.round((uploadedSize / totalSize) * 100).toFixed(
            2
          );
          onProgress({ percent: percent });
        }

        const speed = getSpeed();
        const remainingTime =
          speed != 0
            ? Math.ceil((totalSize - uploadedSize) / speed) + "s"
            : "未知";
        console.log(
          "剩余大小：",
          (totalSize - uploadedSize) / 1024 / 1024,
          "mb"
        );
        console.log("当前速度：", (speed / 1024 / 1024).toFixed(2), "mbps");
        console.log("预计完成：", remainingTime);
      };

      return new Promise((resolve) => {
        const failArr = [];
        const queue = Queue(5, {
          retry: 3, //Number of retries
          retryIsJump: false, //retry now?
          workReject: function (reason, queue) {
            failArr.push(reason);
          },
          queueEnd: function (queue) {
            resolve(failArr);
          },
        });
        fileUploadChunkQueue[file.uid] = queue;
        for (let partNumber = 1; partNumber <= chunkNum; partNumber++) {
          const exitPart = (exitPartList || []).find(
            (exitPart) => exitPart.partNumber == partNumber
          );
          if (exitPart) {
            // 分片已上传完成，累计到上传完成的总额中,同时记录一下上次断点上传的大小，用于计算上传速度
            lastUploadedSize += new Number(exitPart.size);
            updateProcess(exitPart.size);
          } else {
            queue.push(() =>
              uploadNext(partNumber).then((res) => {
                // 单片文件上传完成再更新上传进度
                updateProcess(res.uploadedSize);
              })
            );
          }
        }
        if (queue.getLength() == 0) {
          // 所有分片都上传完，但未合并，直接return出去，进行合并操作
          resolve(failArr);
          return;
        }
        queue.start();
      });
    },
    handleRemoveFile(uploadFile, uploadFiles) {
      // this.$refs.upload.abort();
      this.loading = false;
      this.isAbort = true;
      const queueObject = fileUploadChunkQueue[uploadFile.uid];
      if (queueObject) {
        queueObject.stop();
        fileUploadChunkQueue[uploadFile.uid] = undefined;
      }
    },
    async handleHttpRequest(options) {
      const file = options.file;
      this.loading = true;
      this.isAbort = false;
      const task = await this.getTaskInfo(file);
      if (task) {
        const { finished, path, taskRecord } = task;
        const { fileIdentifier: identifier } = taskRecord;
        if (finished) {
          this.$message.success("文件已存在");
          this.loading = false;
          return path;
        } else {
          const errorList = await this.handleUpload(file, taskRecord, options);
          if (errorList.length > 0) {
            this.loading = false;
            this.$message.error({
              title: "文件上传错误",
              message: "部分分片上次失败，请尝试重新上传文件",
            });
            return;
          }
          const {
            data: { code, data, msg },
          } = await api.merge(identifier);
          this.loading = false;
          if (code === 200) {
            if(this.updateList){
              EventBus.$emit('refreshFile')
            }
            this.$message.success("上传成功！");
            return path;
          } else {
            this.$message.error({
              title: "文件上传错误",
              message: msg,
            });
          }
        }
      } else {
        this.$message.error({
          title: "文件上传错误",
          message: "获取上传任务失败",
        });
      }

      const prom = {};
      prom.abort = () => {};
      return prom;
    },
  },
};
</script>

<style scoped lang="scss">
.upload-demo {
  width: 100%;
}
</style>